﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Web;
using System.Web.Helpers;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using System.Web.Routing;

namespace Mvc4Demo.Extensions
{
    public static class KnockoutControlExtensions
    {
        public static MvcHtmlString KOPartialFor<TModel, TProperty>(this HtmlHelper<TModel> helper,
                                                                    Expression<Func<TModel, TProperty>> expression,
                                                                    string partialViewName)
        {
            var name = ExpressionHelper.GetExpressionText(expression);
            var model = ModelMetadata.FromLambdaExpression(expression, helper.ViewData).Model;
            var viewData = new ViewDataDictionary(helper.ViewData)
                {
                    TemplateInfo = new TemplateInfo
                        {
                            HtmlFieldPrefix = name
                        }
                };

            var oldModelName = RenderingViewModel;

            RenderingViewModel = string.IsNullOrEmpty(oldModelName)
                                     ? name
                                     : string.Format("{0}.{1}", oldModelName, name);

            var mvcHtmlString = helper.Partial(partialViewName, model, viewData);

            RenderingViewModel = oldModelName;

            return mvcHtmlString;
        }


        public static MvcHtmlString KODropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
                                                                         Expression<Func<TModel, TProperty>> expression,
                                                                         object htmlAttributes = null)
        {
            var propertyName = ControlExtensions.GetPropertyName(expression);

            var attributeList = GetAttributeList(htmlAttributes);

            var modelName = htmlHelper.GetKOModelName();

            attributeList.Add("data-bind",
                              string.Format(
                                  "value:{0}.{1}, options: {1}List, optionsText: 'Text', optionsValue: 'Value'",
                                  modelName, propertyName));

            var selectListItems = ControlExtensions.GetSelectListItems(htmlHelper, expression).ToList();

            var knockoutViewModelDictionary = GetKnockoutViewModelDictionary(htmlHelper);

            if (!knockoutViewModelDictionary.ContainsKey(propertyName + "List"))
            {
                var sbJson = GetListViewModelJson(propertyName, selectListItems);
                knockoutViewModelDictionary.Add(propertyName + "List", sbJson);
            }

            return htmlHelper.DropDownListFor(expression, selectListItems, attributeList);
        }

        public static MvcHtmlString KOTextBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
                                                                    Expression<Func<TModel, TProperty>> expression,
                                                                    object htmlAttributes = null, string binding = "value")
        {
            var propertyName = ControlExtensions.GetPropertyName(expression);

            var attributeList = GetAttributeList(htmlAttributes);

            var modelName = htmlHelper.GetKOModelName();

            attributeList.Add("data-bind", string.Format("{2}: {0}.{1}", modelName, propertyName, binding));

            return htmlHelper.TextBoxFor(expression, attributeList);
        }


        public static MvcHtmlString KOCheckBoxFor<TModel>(this HtmlHelper<TModel> htmlHelper,
                                                          Expression<Func<TModel, bool>> expression,
                                                          object checkboxAttributes = null,
                                                          object labelAttributes = null)
        {
            var propertyName = ControlExtensions.GetPropertyName(expression);

            var attributeList = GetAttributeList(checkboxAttributes);

            var modelName = htmlHelper.GetKOModelName();

            attributeList.Add("data-bind", string.Format("checked: {0}.{1}", modelName, propertyName));

            var htmlString = htmlHelper.CheckBoxFor(expression, attributeList);

            var displayName = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData).DisplayName;

            var lblAttributes = ConvertDictionaryToAttributes(GetAttributeList(labelAttributes));

            return MvcHtmlString.Create(string.Format("<{0} {3}>{1}{2}</{0}>", "label",
                                                      htmlString, displayName,
                                                      lblAttributes));
        }

        private static RouteValueDictionary GetAttributeList(object htmlAttributes)
        {
            return htmlAttributes != null
                       ? HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)
                       : new RouteValueDictionary();
        }

        public static MvcHtmlString KORadioButtonFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
                                                                        Expression<Func<TModel, TProperty>> expression,
                                                                        object htmlAttributes = null,
                                                                        object groupHtmlAttributes = null)
        {
            var propertyName = ControlExtensions.GetPropertyName(expression);

            var attributeList = GetAttributeList(htmlAttributes);

            var modelName = htmlHelper.GetKOModelName();

            attributeList.Add("data-bind", string.Format("checked:{0}.{1}", modelName, propertyName));

            var selectListItems = ControlExtensions.GetSelectListItems(htmlHelper, expression)
                                                   .ToList();

            var groupHtmlAttributeList = GetAttributeList(groupHtmlAttributes);

            var groupHtmlAttributeString = ConvertDictionaryToAttributes(groupHtmlAttributeList);

            var htmlStringbuilder = new StringBuilder(1024);
            htmlStringbuilder.AppendFormat("<div{0}>", groupHtmlAttributeString);

            //select first if none of are selected.
            if (!selectListItems.Any(x => x.Selected) && selectListItems.Any())
            {
                selectListItems.First().Selected = true;
            }

            foreach (var item in selectListItems)
            {
                if (item.Selected)
                {
                    attributeList.Add("checked", "checked");
                }
                var radioButtonString = InputExtensions.RadioButtonFor(htmlHelper,
                                                                       expression,
                                                                       item.Value,
                                                                       attributeList);
                htmlStringbuilder.AppendFormat("<div>{0}{1}</div>", radioButtonString, item.Text);
            }

            htmlStringbuilder.Append("</div>");

            return MvcHtmlString.Create(htmlStringbuilder.ToString());
        }

        public static MvcHtmlString KOHiddenFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
                                                                   Expression<Func<TModel, TProperty>> expression)
        {
            var modelName = htmlHelper.GetKOModelName();
            var propertyName = ControlExtensions.GetPropertyName(expression);
            var attributeList = new RouteValueDictionary
                {
                    {"data-bind", string.Format("value: {0}.{1}", modelName, propertyName)}
                };

            return InputExtensions.HiddenFor(htmlHelper, expression, attributeList);
        }

        public static MvcHtmlString KODisplayFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
                                                                    Expression<Func<TModel, TProperty>> expression,
                                                                    object htmlAttributes = null, string binding = "text")
        {
            var propertyName = ControlExtensions.GetPropertyName(expression);

            var attributeList = GetAttributeList(htmlAttributes);

            var modelName = htmlHelper.GetKOModelName();

            attributeList.Add("data-bind", string.Format("{2}: {0}.{1}", modelName, propertyName, binding));

            var value = htmlHelper.DisplayFor(expression, attributeList);

            var attributes = ConvertDictionaryToAttributes(attributeList);

            return MvcHtmlString.Create(string.Format("<span {1}>{0}</span>", value, attributes));
        }

        public static MvcHtmlString KODisplayLabelValueFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
                                                                              Expression<Func<TModel, TProperty>> expression,
                                                                              object htmlAttributes = null, string binding = "text")
        {
            var propertyName = ControlExtensions.GetPropertyName(expression);

            var attributeList = GetAttributeList(htmlAttributes);

            var modelName = htmlHelper.GetKOModelName();

            attributeList.Add("data-bind", string.Format("{2}: {0}.{1}", modelName, propertyName, binding));

            var display = htmlHelper.DisplayNameFor(expression);
            var value = htmlHelper.DisplayFor(expression, attributeList);

            var attributes = ConvertDictionaryToAttributes(attributeList);

            return MvcHtmlString.Create(string.Format("{0} : <span {2}>{1}</span>", display, value, attributes));
        }

        public static string ConvertDictionaryToAttributes(RouteValueDictionary dictionary)
        {
            var attributes = dictionary.Any() ? " " : string.Empty;
            return dictionary.Aggregate(attributes, (c, att) => c + string.Format("{0}='{1}'", att.Key, att.Value));
        }

        public const string KnockoutViewModelString = "KnockOutViewModel";

        public static void KOBuildViewModel<TModel>(this HtmlHelper<TModel> htmlHelper)
        {
            var modelName = htmlHelper.GetKOModelName();

            BuildViewModel(htmlHelper.ViewData.Model, modelName);
        }

        public static void BuildViewModel(object model, string name = "model")
        {
            if (model == null || KnockoutViewModelDictionary.ContainsKey(name))
                return;

            var modelScript = new StringBuilder(string.Format("\n\tvar {0} = function () {{\n\t\tvar self = this;", name));
            var modelUpdateScript = new StringBuilder("\n\t\tself.Update = function (data){");

            var modelAddUpdScript = new StringBuilder();

            foreach (var pinfo in model.GetType().GetProperties())
            {
                var koPropertyName = "ko.observable()";
                var updateObservableFormat = string.Empty;
                var newObservableFormat = string.Empty;

                if (pinfo.PropertyType.IsClass && pinfo.PropertyType != typeof(string))
                {
                    if (pinfo.PropertyType.IsGenericType)
                    {
                        var genericParamType = pinfo.PropertyType.GenericTypeArguments[0];
                        var genericParamobj = Activator.CreateInstance(genericParamType);
                        var viewModelName = genericParamType.Name.Replace("ViewModel", "");
                        BuildViewModel(genericParamobj, viewModelName);

                        var typeObj = Activator.CreateInstance(pinfo.PropertyType);

                        if (typeObj is IEnumerable)
                        {
                            koPropertyName = string.Format("ko.observableArray([new {0}()])", viewModelName);
                            modelAddUpdScript.AppendFormat("\n\t\tself.add{1} = function() {{\n\t\t\tself.{0}.push(new {1}());\n\t\t\t }};",
                                                           pinfo.Name, viewModelName);
                            modelAddUpdScript.AppendFormat("\n\t\tself.remove{1} = function(line) {{\n\t\t\t self.{0}.remove(line);\n\t\t\t }};",
                                                           pinfo.Name, viewModelName);

                            var newObserverObj =
                                string.Format(
                                    "\n\t\t\t\t var obj =data.{0}[i]; var newObserver = new {1}();\n\t\t\t\tnewObserver.Update(obj);\n\t\t\t\tself.{0}.push(newObserver);",
                                    pinfo.Name, viewModelName);

                            var forUpdateLoop = string.Format("\n\t\t\tfor (i=0;i<data.{0}.length;i++) {{\n\t\t\t {1} \n\t\t\t}}",
                                                              pinfo.Name, newObserverObj);
                            updateObservableFormat = string.Format("\n\t\t\tself.{0}.removeAll();", pinfo.Name) + forUpdateLoop;
                        }
                    }
                    else
                    {
                        var genericParamobj = Activator.CreateInstance(pinfo.PropertyType);
                        var viewModelName = pinfo.PropertyType.Name.Replace("ViewModel", "");
                        koPropertyName = string.Format("new {0}()", viewModelName);
                        updateObservableFormat = string.Format("\n\t\t\tself.{0}.Update(data.{0});", pinfo.Name);
                        BuildViewModel(genericParamobj, viewModelName);
                    }
                }
                else if (pinfo.PropertyType.IsValueType && pinfo.PropertyType == typeof(DateTime))
                {
                    koPropertyName = "ko.observable(new Date())";
                    updateObservableFormat = string.Format("\n\t\t\tself.{0}(jsonDateToDate(data.{0}));", pinfo.Name);
                }

                modelScript.Append(string.IsNullOrEmpty(newObservableFormat)
                                       ? string.Format("\n\t\tself.{0} = {1};", pinfo.Name, koPropertyName)
                                       : newObservableFormat);

                modelUpdateScript.Append(string.IsNullOrEmpty(updateObservableFormat)
                                             ? string.Format("\n\t\t\tself.{0}(data.{0});", pinfo.Name)
                                             : updateObservableFormat);
            }

            modelUpdateScript.Append("\n\t\t}");

            modelScript.Append(modelUpdateScript);
            modelScript.Append(modelAddUpdScript);

            modelScript.Append("\n\t}");

            KnockoutViewModelDictionary.Add(name, modelScript);

        }

        public static StringBuilder GetListViewModelJson(string propertyName, IEnumerable<SelectListItem> selectListItems)
        {
            var koViewModel = new StringBuilder(string.Format("\n\t\t{0}.{1}List = [", KnockoutViewModelString, propertyName));

            foreach (var listItem in selectListItems)
            {
                koViewModel.Append('{');
                koViewModel.AppendFormat("Value:'{0}', Text:'{1}',", listItem.Value, listItem.Text);
                koViewModel.Append("},");
            }
            koViewModel.Append("];");
            return koViewModel;
        }

        public static MvcHtmlString KOGenerateViewModel<TModel>(this HtmlHelper<TModel> htmlHelper,
                                                                bool applyBindings = false)
        {
            if (!htmlHelper.ViewContext.HttpContext.Items.Contains(KnockoutViewModelString))
                return MvcHtmlString.Empty;

            var modelName = htmlHelper.GetKOModelName();

            var knockoutViewModelDictionary = GetKnockoutViewModelDictionary(htmlHelper);

            if (knockoutViewModelDictionary == null)
                return MvcHtmlString.Empty;

            var koViewModelScript =
                new StringBuilder(string.Format("\n\tif ({0} === undefined) {{\n\t var {0} = {{}};\n\t}}",
                                                KnockoutViewModelString));

            foreach (var kp in knockoutViewModelDictionary)
            {
                koViewModelScript.Append(kp.Value);
            }

            koViewModelScript.AppendFormat("\n\t{0}.{1} = new {1}();\n", KnockoutViewModelString, modelName);
            if (applyBindings)
            {
                koViewModelScript.Append(
                    string.Format("$(document).ready(function () {{\n\tko.applyBindings({0});\n\t}});",
                                  KnockoutViewModelString));
            }
            return MvcHtmlString.Create(koViewModelScript.ToString());
        }

        public static Dictionary<string, StringBuilder> KnockoutViewModelDictionary
        {
            get
            {
                var httpContext = HttpContext.Current;
                if (!httpContext.Items.Contains(KnockoutViewModelString))
                {
                    var objectDictionary = new Dictionary<string, StringBuilder>();
                    httpContext.Items.Add(KnockoutViewModelString, objectDictionary);
                    return objectDictionary;
                }
                return httpContext.Items[KnockoutViewModelString] as Dictionary<string, StringBuilder>;

            }
        }

        #region private methods and properties

        private static Dictionary<string, StringBuilder> GetKnockoutViewModelDictionary<TModel>(HtmlHelper<TModel> htmlHelper)
        {
            if (!htmlHelper.ViewContext.HttpContext.Items.Contains(KnockoutViewModelString))
            {
                var objectDictionary = new Dictionary<string, StringBuilder>();
                htmlHelper.ViewContext.HttpContext.Items.Add(KnockoutViewModelString, objectDictionary);
                return objectDictionary;
            }
            return htmlHelper.ViewContext.HttpContext.Items[KnockoutViewModelString] as Dictionary<string, StringBuilder>;
        }

        private const string RenderingViewModelString = "RenderingViewModel";

        private static string RenderingViewModel
        {
            get
            {
                var httpContext = HttpContext.Current;
                if (httpContext.Items.Contains(RenderingViewModelString))
                {
                    return httpContext.Items[RenderingViewModelString] as string;
                }
                return string.Empty;
            }
            set
            {
                var httpContext = HttpContext.Current;
                if (!httpContext.Items.Contains(RenderingViewModelString))
                {
                    httpContext.Items.Add(RenderingViewModelString, value);
                }
                else
                {
                    httpContext.Items[RenderingViewModelString] = value;
                }
            }
        }

        private static string GetKOModelName<TModel>(this HtmlHelper<TModel> helper)
        {
            if (string.IsNullOrEmpty(RenderingViewModel))
            {
                RenderingViewModel = helper.ViewData.Model.GetType().Name.Replace("ViewModel", "");
            }

            return RenderingViewModel;

            //var prefix = helper.ViewData.TemplateInfo.HtmlFieldPrefix;
            //return string.IsNullOrEmpty(prefix) ? RenderingViewModel : string.Format("{0}.{1}", RenderingViewModel, prefix);
        }

        #endregion


        public static MvcHtmlString GenerateJsonModel<TModel>(this HtmlHelper<TModel> htmlHelper, string model = "model")
        {
            const string jsonModelFormat = "var {0} = {1};";

            var jsonData = htmlHelper.Raw(Json.Encode(htmlHelper.ViewData.Model));

            return MvcHtmlString.Create(string.Format(jsonModelFormat, model, jsonData));

        }
    }
}